Skip to content

Vue 组件通信

作者:青见春山
发表于:2026-07-29
字数统计:4500 字
预计阅读16分钟

涵盖 Vue 父子通信、跨组件通信、全局状态管理各种方式,包括 Vue2 / Vue3 差异、provide/inject 响应式处理、事件总线迁移、$attrs 与 props 区别。

一、标准面试回答(1 分钟)

Vue 组件通信的核心是"单向数据流",但根据组件关系(父子、兄弟、跨层级)和场景复杂度,有多种实现方式。常见方式分为三类:父子通信、跨组件通信、全局状态管理。

父子通信

  • 父传子:用 props,子组件通过 props 接收数据
  • 子传父:用 $emit 触发自定义事件,父组件监听
  • 父调子:用 ref 获取子组件实例,调用子组件方法或访问数据
  • 子调父:也可以通过 $parent 访问父组件实例(但耦合度高,不推荐)

跨组件通信(祖孙、兄弟)

  • 依赖注入provideinject,祖先组件提供数据,后代组件直接注入,适合跨多级传递;普通值不会自动变成响应式,传入已有的 ref/reactive 对象则可保留响应性
  • 事件总线:Vue 2 中用 EventBusnew Vue() 作为事件中心),Vue 3 中推荐用 mitttiny-emitter 替代
  • $attrs / $listeners:用于透传属性和事件,封装高阶组件时很实用

全局状态管理

  • Vuex / Pinia:适用于复杂的跨组件共享状态。Pinia 是当前 Vue 官方推荐的新应用状态库;现有 Vuex 项目可以按版本支持与迁移成本决定是否迁移,而不是简单按 Vue 2/3 二分

其他方式

  • v-model:可理解为 prop 与更新事件的约定语法;Vue 2 组件默认使用 value / input,Vue 3 默认使用 modelValue / update:modelValue 并支持多个 v-model
  • 插槽(slot):父组件向子组件传递模板内容,作用域插槽还能让子组件向父组件回传数据
  • $root:访问根实例,一般只用于极简 demo,不推荐生产使用

选择原则:优先用 props/$emit 解决父子通信;跨多级用 provide/inject;复杂全局状态用 Pinia/Vuex;简单跨组件用事件总线但要谨慎。

二、20 秒极简版

Vue 组件通信方式:父子用 props/$emit/ref;跨层级用 provide/inject$attrs;兄弟组件可通过共同父组件中转或用事件总线;复杂全局状态用 Pinia/Vuex;还有 v-model、插槽等语法糖方式。

三、分类速查表

通信方式适用场景Vue 2 / Vue 3 差异
props / $emit父子通信基本一致
ref父调子Vue 3 <script setup> 默认关闭,需 defineExpose
provide / inject跨层级两者都不会把普通值自动变响应式;Vue 3 常直接提供 ref/reactive
事件总线简单跨组件Vue 2: new Vue();Vue 3: mitt
$attrs / $listeners属性透传Vue 3 合并 $listeners$attrs
v-model父子双向绑定Vue 3 支持多个
作用域插槽子传数据给父渲染语法一致
Vuex / Pinia全局状态Vue 3 推荐 Pinia

四、追问 1:provide 和 inject 怎么实现响应式数据传递?

provide / inject 本身不会给普通值添加响应性,但也不会剥离所提供对象已有的响应能力。若要让后代响应变化,应提供 ref、reactive/observable 对象或能读取响应式来源的值,并把修改逻辑尽量留在提供方。

Vue 2 中的做法

传入响应式对象,或者用 Vue.observable 包裹:

JavaScript
// 基础写法(不响应式)
provide() {
  return {
    user: this.user,        // 直接传非响应式
    updateUser: this.updateUser
  }
}

// 更好的做法:传入响应式对象本身
provide() {
  return {
    userData: this.$data.userInfo  // 传入响应式对象
  }
}

Vue 3 中的做法

provide 可以直接传入 refreactive 对象,响应性会自动保持。后代用 inject 拿到后,直接使用即可。如果希望只读,可以配合 readonly 包裹。

JavaScript
// Vue 3 组合式 API
provide('user', userRef)  // 传入 ref
const user = inject('user')  // 自动保持响应性

另外,如果要在 inject 中修改数据,推荐同时 provide 一个修改方法,遵循"单向数据流"原则,避免后代随意修改祖先数据。

五、追问 2:Vue 2 和 Vue 3 中事件总线的实现有什么区别?

Vue 2 中

可以直接创建一个空的 Vue 实例作为事件总线:

JavaScript
// bus.js
export const bus = new Vue()

// 组件 A 发送
bus.$emit('event', data)

// 组件 B 接收
bus.$on('event', handler)

需要在组件销毁时用 $off 移除监听,避免内存泄漏。

Vue 3 中

Vue 3 移除了 $on$off 等实例方法,不再推荐这种模式。官方建议用第三方库如 mitttiny-emitter 替代。

JavaScript
import mitt from 'mitt'
export const emitter = mitt()

// 发送
emitter.emit('event', data)
// 接收
emitter.on('event', handler)
// 移除
emitter.off('event', handler)

事件总线的使用要谨慎:过度使用会让数据流向变得难以追踪,项目复杂度上升后调试困难。一般推荐在小型项目或非核心场景使用,大型应用还是走 Pinia 或 Vuex。

六、追问 3:$attrs 和 props 的区别?什么场景用 $attrs?

核心区别props 是子组件声明接收的属性,$attrs 是未被组件声明消费的透传属性。Vue 3 中它还可能包含 onXxx 事件监听器;通过 emits 声明的监听器会从 $attrs 排除。Vue 2 的监听器则主要位于 $listeners

在 Vue 2 中,$attrs 还配合 $listeners(事件监听器)一起使用;Vue 3 中 $listeners 合并到 $attrs 里。

典型场景:封装高阶组件做属性透传

封装一个 BaseButton 组件,它内部是一个原生 <button>,我们希望父组件传给 BaseButton 的所有属性(如 disabledtypeclass)都能自动落到原生 <button> 上,不需要在 props 里一个个声明。

Plain
<template>
  <button v-bind="$attrs">
    <slot />
  </button>
</template>
<script>
export default {
  inheritAttrs: false  // 避免根元素自动继承
}
</script>

使用 $attrs 可以大大减少 props 的重复声明,让组件封装更干净。配合 inheritAttrs: false 还能控制属性是否自动挂载到根元素上。

七、易错点

  • 只列方式不说适用场景:一口气说出 8 种方式但不知道什么时候用哪个
  • 混淆 Vue 2 和 Vue 3 的差异:Vue 3 中 $listeners 合并到 $attrs,事件总线不再内置,Pinia 取代 Vuex 成为官方推荐
  • 忽略 provide/inject 的响应性来源:普通值不会自动响应,已有 ref/reactive 对象则会保留响应性
  • 事件总线的内存泄漏不提:能主动提到"组件销毁时 $off 清理监听"
  • $attrs 和 props 的概念混淆:有人以为 $attrs 是 props 的超集,其实 props 声明过的属性会从 $attrs 中移除
  • 过度使用全局状态管理:能说出"简单场景优先用 props/$emit,复杂场景再用状态管理"才体现工程判断力
  • 忘记 v-model 和插槽也是通信方式:作用域插槽能加分
  • 父子通信只提 props/$emit 不提 ref:父组件调用子组件方法也是常见场景

九、前端权限控制完整方案

前端权限控制本质是根据用户角色 / 权限点,控制页面 / 菜单 / 按钮的展示与可操作性。通常分为两层:路由级权限(页面是否能访问)和按钮级权限(操作是否能执行)。

一、整体架构与前后端配合

配合流程

  1. 登录:用户登录成功后,后端返回一个 token + 当前用户的权限点列表 / 角色列表(如 ['user:add', 'user:edit']['admin'])。
  2. 存储:前端可以把 UI 所需权限点放入 Vuex / Pinia;凭据放在哪里要按威胁模型设计。localStorage 中的 token 可被同源脚本读取,存在 XSS 窃取风险,不应把它当成默认安全方案。
  3. 校验:路由跳转前和组件渲染前,根据权限点判断是否允许。
  4. 渲染:根据权限点动态生成菜单、过滤路由、控制按钮显示。

二、页面 / 菜单级别权限 & 动态路由(Dynamic Routing)

1. 路由划分

  • 静态路由:所有用户都能访问的页面(如登录页、404、个人中心)。
  • 动态路由:根据用户权限动态添加的路由(通常来自后端接口返回的菜单树)。

2. 动态路由的两种常见模式

  • 后端下发路由表:后端根据用户角色返回对应的完整路由配置(含 component 路径 / component name),前端 addRoute 动态挂载。
  • 前端维护所有路由 + 权限过滤:前端维护所有可能的路由,根据权限点过滤后挂载。

3. 核心实现步骤(配合路由守卫)

JavaScript
router.beforeEach(async (to, from, next) => {
    const token = localStorage.getItem('token')
    if (!token) return next('/login')

    // 判断是否已经加载过动态路由
    if (!store.state.hasLoadedRoutes) {
        try {
            // 1. 请求后端拿当前用户的路由表
            const routes = await api.getUserRoutes()
            // 2. 动态挂载
            routes.forEach(route => router.addRoute(route))
            // 3. 标记已加载
            store.commit('setLoadedRoutes', true)
            // 4. 重新导航(因为 addRoute 后路由未生效)
            next({ ...to, replace: true })
        } catch (e) {
            next('/login')
        }
    } else {
        next()
    }
})

三、按钮级别权限 & 自定义指令

1. 编写自定义指令 v-permission

JavaScript
// directives/permission.js
import store from '@/store'

function checkPermission(el, binding) {
    // binding.value: ['user:add', 'user:edit'] 或 'user:add'
    const required = Array.isArray(binding.value)
        ? binding.value
        : [binding.value]

    const userPerms = store.state.user.permissions || []
    const hasPermission = required.some(p => userPerms.includes(p))

    if (!hasPermission) {
        // 移除 DOM 元素
        el.parentNode?.removeChild(el)
    }
}

export default {
    name: 'permission',
    inserted: checkPermission,
    update: checkFunction  // 当表达式变化时重新校验
}

全局注册:

JavaScript
import permission from '@/directives/permission'
Vue.directive('permission', permission)

2. 在组件中使用

Vue
<template>
    <div>
        <!-- 单权限校验 -->
        <el-button v-permission="'user:add'">新增用户</el-button>

        <!-- 满足任一权限即可 -->
        <el-button v-permission="['user:add', 'user:edit']">编辑用户</el-button>
    </div>
</template>

3. 特殊情况:非 DOM 元素的权限控制

对于下拉菜单项、Tabs 标签等非 DOM 元素,自定义指令不生效时,可用 v-if + 工具函数:

JavaScript
// utils/auth.js
export function hasPermission(perm) {
    const userPerms = store.state.user.permissions || []
    return userPerms.includes(perm)
}
Vue
<el-dropdown-item v-if="hasPermission('user:delete')">删除</el-dropdown-item>

总结

  • 路由级权限:核心是 router.beforeEach + router.addRoute 动态挂载。
  • 按钮级权限:优先用 v-permission 自定义指令,复杂场景配合 v-if + 工具函数
  • 数据权限:列表接口本身就要带权限过滤,前端只是 UI 兜底(永远不要相信前端权限校验)。

十、关联文档